package server.core;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * The ClientHandler class defines an object that handles a single client
 * connection. Implements the {@link Runnable} interface so that each new
 * ClientHandler can be run in an independent thread.
 * 
 * @author Clayton Edward Taylor
 */
public class ClientHandler implements Runnable {

	private Socket socket;

	/**
	 * Creates a new ClientHandler that will interact with the given socket.
	 * 
	 * @param socket - The client that will be handled.
	 * @see            ClientHandler
	 */
	public ClientHandler(Socket socket) {

		this.socket = socket;
	}

	/**
	 * Handles a single problem message from a client.
	 * 
	 * @see ClientHandler
	 */
	@Override
	public void run() {

		try {
			InputStream is = socket.getInputStream();
			OutputStream os = socket.getOutputStream();
			handleClient(is, os);
		} catch (IOException e) {
			return;
		} finally {
			// Always try to close the client socket to avoid leaks
			try {
				socket.close();
			} catch (IOException e) {
				return;
			}
		}
	}
	
	/**
	 * Handles a client by receiving a problem message, retrieving the
	 * the appropriate {@link ProblemManager} for the requested problem, and
	 * then using it to parse the problem and produce a response to the client
	 * (either a solution or an error).
	 * 
	 * @param is - The InputStream of the socket
	 * @param os - The OutputStream of the socket
	 * @see        ClientHandler
	 */
	private void handleClient(InputStream is, OutputStream os) {
		
		try {
			ProblemMessage msg = readProblemMessage(is);
			ProblemManager pm = Managers.getManager(msg.getType());
			if (pm == null) {
				throw new ProblemTypeException();
			}
			Problem p = runParser(msg, pm);
			Solution s = findSolution(p, pm, msg.getTimeOut());
			if (s == null) {
				throw new SolverFailureException();
			}
			sendResponse(os, "SOLUTION", s.toString());
			pm.insertCache(p, s);
			
		} catch (IOException e) {
			sendResponse(os, "ERROR", Errors.READING.toString());
		} catch (ExecutionException e) {
			sendResponse(os, "ERROR", Errors.PARSER.toString());
		} catch (ProblemTypeException e) {
			sendResponse(os, "ERROR", Errors.TYPE.toString());
		} catch (InterruptedException e) {
			sendResponse(os, "ERROR", Errors.INTERRUPTED.toString());
		} catch (TimeoutException e) {
			sendResponse(os, "ERROR", Errors.TIMEOUT.toString());
		} catch (SolverFailureException e) {
			sendResponse(os, "ERROR", Errors.SOLVER.toString());
		}
	}

	/**
	 * Reads the problem message from the client, storing it in a new
	 * {@link ProblemMessage} object.
	 * 
	 * @param is - The InputStream of the socket
	 * @return     The problem message read in
	 * @throws     IOException If an I/O error occurs
	 * @see        ClientHandler
	 */
	private ProblemMessage readProblemMessage(InputStream is) 
			throws IOException {

		BufferedReader br = new BufferedReader(new InputStreamReader(is));
		Scanner parser = new Scanner(br.readLine());
		String type = parser.next().trim();
		long timeOut = parser.nextLong();
		int size = parser.nextInt();
		parser.close();
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < size; ++i) {
			sb.append((char) br.read());
		}
		return new ProblemMessage(type, timeOut, sb.toString());
	}
	
	/**
	 * Runs the problem parser that is returned by 
	 * {@link ProblemManager#getProblemParser(String)} on the problem data
	 * from the given {@link ProblemMessage}.
	 * 
	 * @param msg - The ProblemMessage from the client
	 * @param pm  - The ProblemManager to use
	 * @return      The parsed Problem object
	 * @throws      InterruptedException If the current thread was interrupted
	 *              while waiting
	 * @throws      ExecutionException If the computation threw an exception
	 * @throws      TimeoutException If the wait timed out
	 * @see         ClientHandler
	 */
	private Problem runParser(ProblemMessage msg, ProblemManager pm) 
			throws InterruptedException, ExecutionException, TimeoutException {
		
		Problem p;
		ExecutorService es = Executors.newSingleThreadExecutor();
		try {
			long timeBegin = System.nanoTime();
			Future<Problem> f = es.submit(pm.getProblemParser(msg.getData()));
			p = f.get(msg.getTimeOut(), TimeUnit.SECONDS);
			long timeElapsed = System.nanoTime() - timeBegin;
			msg.setTimeOut(msg.getTimeOut() - TimeUnit.NANOSECONDS.toSeconds(timeElapsed));
		} finally {
			es.shutdownNow();
		}
		return p;
	}
	
	/**
	 * Finds a {@link Solution} by running the cache query and solve dispatch
	 * objects returned by {@link ProblemManager#getCacheQuery(Problem)} and
	 * {@link ProblemManager#getSolverDispatch(Problem)}.
	 * 
	 * @param p       - The Problem to find a Solution for
	 * @param pm      - The ProblemManager to use
	 * @param timeOut - The maximum time allowed to find a solution
	 * @return          A Solution object returned by either the cache or 
	 *                  solver, or <code>null</code> if neither succeeds
	 * @throws          InterruptedException If the current thread was 
	 *                  interrupted while waiting
	 * @throws          TimeoutException If the wait timed out
	 * @see             ClientHandler
	 */
	private Solution findSolution(Problem p, ProblemManager pm, long timeOut)
			throws InterruptedException, TimeoutException {
		
		Solution s;
		ExecutorService es = Executors.newFixedThreadPool(2);
		Set<Callable<Solution>> tasks = new HashSet<Callable<Solution>>();
		tasks.add(pm.getCacheQuery(p));
		tasks.add(pm.getSolverDispatch(p));
		try {
			s = es.invokeAny(tasks, timeOut, TimeUnit.SECONDS);
		} catch (ExecutionException e) {
			s = null;
		} finally {
			es.shutdownNow();
		}
		return s;
	}
	
	/**
	 * Sends a response message to the client containing the specified type in
	 * the header and the specified data in the body.
	 * 
	 * @param os   - The OutputStream of the client socket
	 * @param type - The type of the response message
	 * @param data - The data to send as the body of the response message
	 * @see          ClientHandler
	 */
	private void sendResponse(OutputStream os, String type, String data) {
		
		PrintWriter pw = new PrintWriter(os);
		pw.println(type + " " + data.length());
		pw.print(data);
		pw.flush();
	}
	
	/**
	 * The ProblemTypeException class defines an internal type of Exception
	 * that occurs when a message has a type that is not recognized.
	 * 
	 * @see ClientHandler
	 */
	@SuppressWarnings("serial")
	private class ProblemTypeException extends Exception { }
	
	/**
	 * The SolverFailureException class defines an internal type of Exception
	 * that occurs when the solver unexpectedly fails.
	 * 
	 * @see ClientHandler
	 */
	@SuppressWarnings("serial")
	private class SolverFailureException extends Exception { }
	
	/**
	 * The Errors enumeration defines all of the errors and their associated
	 * messages to the client.
	 * 
	 * @see ClientHandler
	 */
	private enum Errors {
		READING("Encountered error when reading message\n"),
		PARSER("Failed to parse problem data\n"),
		TYPE("Unrecognized problem type\n"),
		INTERRUPTED("Service shutting down\n"),
		TIMEOUT("Timed out\n"),
		SOLVER("Unexpected failure of solver\n");
		
		private final String msg;
		
		private Errors(String msg) {
			
			this.msg = msg;
		}
		
		public String toString() {
			
			return msg;
		}
	}
}
